﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sharebert.SearchLibrary.Interfaces;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Sharebert.SearchLibrary
{
    /// <summary>
    /// Servers network interface used to serve clients. Can be started and stopped.
    /// Triggers events if a client sends a list with files to share 
    /// or searches files.
    /// </summary>
    public class IndexServer : IIndexServer
    {

        private const int DEFAULT_TIMEOUT = 30;

        private const int DEFAULT_BACKLOG = 10;

        public IndexServer(IPAddress listenAddress, int listenPort)
        {
            if (listenPort < IPEndPoint.MinPort || listenPort > IPEndPoint.MaxPort)
            {
                throw new ArgumentOutOfRangeException("No valid port!");
            }
            this.activeConnections = new ThreadSafeList<IConnection>();
            this.IP = listenAddress;
            this.Port = listenPort;
            this.Timeout = DEFAULT_TIMEOUT;
            this.AllowedClients = DEFAULT_BACKLOG;
        }

        public IndexServer(string listenIP, int listenPort)
        {
            IPAddress ip = null;
            if (!IPAddress.TryParse(listenIP, out ip))
            {
                throw new ArgumentException("No valid IP address!");
            }
            if (listenPort < IPEndPoint.MinPort || listenPort > IPEndPoint.MaxPort)
            {
                throw new ArgumentOutOfRangeException("No valid port!");
            }

            this.activeConnections = new ThreadSafeList<IConnection>();
            this.IP = ip;
            this.Port = listenPort;
            this.Timeout = DEFAULT_TIMEOUT;
            this.AllowedClients = DEFAULT_BACKLOG;
        }

        /// <summary>
        /// Opens a network socket and waits for new connections.
        /// </summary>
        /// <exception cref="NetworkException">Server has already been started</exception>
        public void Start()
        {
            if (this.listenConnection != null && this.listenConnection.Connected)
            {
                throw new NetworkException("Already started!");
            }

            this.listenConnection = new Connection();
            this.listenConnection.ReadTimeout = this.Timeout;
            this.listenConnection.NewConnectionEstablished += new EventHandler<ConnectionEventArgs>(HandleNewConnection);

            Console.WriteLine("Start listening...");
            this.listenConnection.Listen(this.IP, this.Port, this.AllowedClients);
        }

        /// <summary>
        /// Stops listening and closes the socket.
        /// </summary>
        public void Stop()
        {
            this.listenConnection.Close();
            foreach (IConnection conn in this.activeConnections)
            {
                conn.Close();
            }
        }

        /// <summary>
        /// Handles new connection docked on the listening socket.
        /// Adds the new connection to connection list and and registers events.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e">The new connection</param>
        private void HandleNewConnection(object sender, ConnectionEventArgs e)
        {
            this.activeConnections.Add(e.Connection);
            e.Connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(ProcessMessage);
            e.Connection.ConnectionTimedOut += new EventHandler<EventArgs>(ProcessConnectionTimedOut);
            e.Connection.ConnectionClosed += new EventHandler<EventArgs>(ProcessConnectionClosed);
        }

        /// <summary>
        /// A client connection has timed out.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessConnectionTimedOut(object sender, EventArgs e)
        {
            IConnection conn = sender as IConnection;
            Console.WriteLine(String.Format("Connection to {0}:{1} timed out. Local end point was {2}:{3}",
                                            conn.RemoteAddress, conn.RemotePort, conn.LocalAddress, conn.LocalPort));
            this.activeConnections.Remove(conn);
        }

        /// <summary>
        /// A client connection has been closed.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessConnectionClosed(object sender, EventArgs e)
        {
            IConnection conn = sender as IConnection;
            Console.WriteLine(String.Format("Connection to {0}:{1} closed. Local end point was {2}:{3}",
                                            conn.RemoteAddress, conn.RemotePort, conn.LocalAddress, conn.LocalPort));
            this.activeConnections.Remove(conn);
        }

        /// <summary>
        /// A client has sent a new message.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProcessMessage(object sender, MessageReceivedEventArgs e)
        {
            IConnection conn = sender as IConnection;
            if (e.Message.Header.Command == Message.COMMAND_FILE_OFFER)
            {
                FileOfferList list = (FileOfferList)Serialisation.Deserialize(e.Message.Payload, typeof(FileOfferList));
                if (this.OfferReceived != null)
                {
                    this.OfferReceived.Invoke(this, new OfferedFilesEventArgs(new ClientHandler(conn), list));
                }
            }
            else if (e.Message.Header.Command == Message.COMMAND_SEARCH_REQUEST)
            {
                SearchRequest request = (SearchRequest)Serialisation.Deserialize(e.Message.Payload, typeof(SearchRequest));
                if (this.SearchReceived != null)
                {
                    this.SearchReceived.Invoke(this, new SearchReceivedEventArgs(new ClientHandler(conn), request.Regex));
                }
            }
            else
            {
                Console.WriteLine("Received message with invalid type: " + e.Message.Header.Command);
            }
        }


        #region Properties and private member

        public IPAddress IP { get; private set; }

        public int Port { get; private set; }

        /// <summary>
        /// Sending and receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int Timeout { get; set; }

        /// <summary>
        /// The maximum number of parallel connections. Must be a value between 1 and 100.
        /// </summary>
        public uint AllowedClients
        {
            get { return this.backlog; }
            set 
            {
                if (value > 0 && value <= 100)
                {
                    this.backlog = value;
                }
            }
        }

        private IConnection listenConnection;

        private uint backlog;

        private ThreadSafeList<IConnection> activeConnections;

        #endregion


        #region Events

        /// <summary>
        /// The server did not respond after Timeout seconds.
        /// </summary>
        public event EventHandler<OfferedFilesEventArgs> OfferReceived;

        /// <summary>
        /// The server responds to a search request.
        /// </summary>
        public event EventHandler<SearchReceivedEventArgs> SearchReceived;

        #endregion

    }
}
